package bf.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;

import bf.program.CodeBlock;
import bf.program.Command;
import bf.program.CommandBlock;
import bf.program.Loop;
import bf.program.ProgElement;
import bf.program.Program;
import bf.program.command.CellDecrement;
import bf.program.command.CellIncrement;
import bf.program.command.OutputCharacter;
import bf.program.command.PointerDecrement;
import bf.program.command.PointerIncrement;
import bf.program.command.ext.CellAdd;
import bf.program.command.ext.PointerAdd;


public class BrainfuckParser {

//	private static final int MAX_BLOCK_SIZE = 1000;
	private static final int optimize = 1;

	public static Program parse(Reader reader) throws IOException {
		Reader bufReader = new BufferedReader(reader);
		if(optimize == 1)
			return new Program(parseCodeBlockOpt(bufReader));
		else
			return new Program(parseCodeBlock(bufReader));
	}

	private static CodeBlock parseCodeBlock(Reader reader) throws IOException {
		ArrayList<ProgElement> peArray = new ArrayList<ProgElement>();
		ArrayList<Command> cmdArray = new ArrayList<Command>();
		int read;
		ParsePE:
		while((read = reader.read()) != -1) {
			char cmd = (char)read;
			switch (cmd) {
				case '>':
					cmdArray.add(Command.PI);
					break;
				case '<':
					cmdArray.add(Command.PD);
					break;
				case '+':
					cmdArray.add(Command.CI);
					break;
				case '-':
					cmdArray.add(Command.CD);
					break;
				case '.':
					cmdArray.add(Command.OC);
					break;
				case '[':
					packCmdBlock(peArray, cmdArray);
					cmdArray = new ArrayList<Command>();
					peArray.add(new Loop(parseCodeBlock(reader)));
					break;
				case ']':
					break ParsePE;
				default:
//					throw new RuntimeException("Unknown command!");
//					System.out.println("Ignore: " + cmd);
					continue;
			}
//			if(peArray.size() >= MAX_BLOCK_SIZE) {
//				CodeBlock pack = new CodeBlock(peArray.toArray(new ProgElement[0]));
//				peArray = new ArrayList<ProgElement>();
//				peArray.add(pack);
//			}
//			if(cmdArray.size() >= MAX_BLOCK_SIZE) {
//				packCmdBlock(peArray, cmdArray);
//				cmdArray = new ArrayList<Command>();
//			}
		}
		packCmdBlock(peArray, cmdArray);
		return new CodeBlock(peArray.toArray(new ProgElement[0]));
	}

	private static CodeBlock parseCodeBlockOpt(Reader reader) throws IOException {
		ArrayList<ProgElement> peArray = new ArrayList<ProgElement>();
		ArrayList<Command> cmdArray = new ArrayList<Command>();
		int read;
		int count;
		ParsePE:
		while((read = reader.read()) != -1) {
			char cmd = (char)read;
			switch (cmd) {
				case '>':
					count = lookAhead(reader, cmd);
					cmdArray.add(new PointerAdd(count));
					break;
				case '<':
					count = lookAhead(reader, cmd);
					cmdArray.add(new PointerAdd(-count));
					break;
				case '+':
					count = lookAhead(reader, cmd);
					cmdArray.add(new CellAdd(count));
					break;
				case '-':
					count = lookAhead(reader, cmd);
					cmdArray.add(new CellAdd(-count));
					break;
				case '.':
					cmdArray.add(Command.OC);
					break;
				case '[':
					if(isClearCell(reader)){
						cmdArray.add(Command.CLR);
					} else {
						packCmdBlock(peArray, cmdArray);
						cmdArray = new ArrayList<Command>();
						peArray.add(new Loop(parseCodeBlockOpt(reader)));
					}
					break;
				case ']':
					break ParsePE;
				default:
//					throw new RuntimeException("Unknown command!");
//					System.out.println("Ignore: " + cmd);
					continue;
			}
		}
		packCmdBlock(peArray, cmdArray);
		return new CodeBlock(peArray.toArray(new ProgElement[0]));
	}

	/**
	 * @param reader
	 * @return true if identified a cell clear pattern [-]
	 * @throws IOException
	 */
	private static boolean isClearCell(Reader reader) throws IOException {
		reader.mark(2);
		if(reader.read() == '-' && reader.read() == ']') {
			return true;
		} else {
			reader.reset();
			return false;
		}
	}

	/**
	 * @param reader
	 * @param cmd
	 * @return count for same cmd look ahead
	 * @throws IOException
	 */
	private static int lookAhead(Reader reader, char cmd) throws IOException {
		int count = 1;
		while(true) {
			reader.mark(1);
			if(reader.read() != cmd) break;
			count++;
		}
		reader.reset();
		return count;
	}

	private static void packCmdBlock(ArrayList<ProgElement> peArray,
			ArrayList<Command> cmdArray) {
		peArray.add(new CommandBlock(cmdArray.toArray(new Command[0])));
	}

}
